當我們開始將 Schematic 與 Angular 專案整合後,就會需要處理越來越多的細節。尤其是在測試時,會更需要去模擬各種情境。
就拿我們昨天做的 Schematic 來說,昨天的 Schematic 規格是:
當我們在 Angular 專案中使用這個 schematic 時,我們希望它會在正確的路徑下產生檔案。
正確的路徑的定義是,如果我們沒有指定專案,則它會在預設專案的路徑底下產生檔案;如果有指定專案,則會在該專案的路徑底下產生檔案。
因此,我們需要在測試中模擬 Angular 專案環境。
先來看看我們目前的測試程式:
import { Tree } from '@angular-devkit/schematics';
import { SchematicTestRunner } from '@angular-devkit/schematics/testing';
import { strings } from '@angular-devkit/core';
import * as path from 'path';
const collectionPath = path.join(__dirname, '../collection.json');
describe('hello-world', () => {
it('成功產出檔案,則檔名為/hello-leo-chen.component.ts', () => {
const name = 'LeoChen';
const runner = new SchematicTestRunner('schematics', collectionPath);
const tree = runner.runSchematic('hello-world', { name: name }, Tree.empty());
const dasherizeName = strings.dasherize(name);
const fullFileName = `/hello-${dasherizeName}.component.ts`;
expect(tree.files).toContain(fullFileName);
const fileContent = tree.readContent(fullFileName);
expect(fileContent).toMatch(/hello-leo-chen/);
expect(fileContent).toMatch(/HelloLeoChenComponent/);
});
});
從上述的測試程式碼可以很明顯地看得出來,我們目前的測試碼並沒有辦法模擬 Angular 專案環境,那要怎麼辦呢?
筆者想先反問大家一件事,請問大家都怎麼建立 Angular 專案的?是不是都是用 Angular CLI 的 ng new
指令來產生專案的?
那在測試的時候,也一樣 ng new
一個就好啦!但該怎麼做呢?
平常當我們在使用 ng new
的時候,其實它會去使用 workspace
與 application
這兩個指令,所以我們也需要模仿它使用這兩個指令。
首先需要引入相關資源:
import { Schema as ApplicationOptions, Style } from '@schematics/angular/application/schema';
import { Schema as WorkspaceOptions } from '@schematics/angular/workspace/schema';
接著先把參數準備好:
describe('hello-world', () => {
const workspaceOptions: WorkspaceOptions = {
name: 'workspace', // 不重要的名字,隨便取,不影響測試結果
newProjectRoot: 'projects', // 專案裡,所有 App 的根目錄,可以隨便取,驗證時會用到
version: '0.1.0', // 不重要的版號,隨便取,不影響測試結果
};
const appOptions: ApplicationOptions = {
name: 'hello', // 專案名稱
inlineStyle: false, // true or false 都可以,不影響測試結果
inlineTemplate: false, // true or false 都可以,不影響測試結果
routing: false, // true or false 都可以,不影響測試結果
style: Style.Css, // Css / Less / Sass / scss / styl 都可以,不影響測試結果
skipTests: false, // true or false 都可以,不影響測試結果
skipPackageJson: false, // true or false 都可以,不影響測試結果
};
it('成功產出檔案,則檔名為/hello-leo-chen.component.ts', () => {
// 略
});
});
然後呼叫 SchematicTestRunner
的 runExternalSchematic
方法,並給予相關參數令其產生出 Angular 專案,並驗證結果:
describe('hello-world', () => {
// 略
it('成功在預設專案路徑底下產出檔案', () => {
const options: HelloWorldSchema = { name: 'feature/Leo Chen' };
const runner = new SchematicTestRunner('schematics', collectionPath);
let appTree = runner.runExternalSchematic(
'@schematics/angular',
'workspace',
workspaceOptions
);
appTree = runner.runExternalSchematic(
'@schematics/angular',
'application',
appOptions,
appTree
);
const tree = runner.runSchematic('hello-world', options, Tree.empty());
expect(tree.files).toContain('/projects/hello/src/app/feature/hello-leo-chen.component.ts');
});
});
結果:
為什麼會錯?!
這是因為 runSchematic
與 runExternalSchematic
這兩個函式是同步的,但 Schematics 在處理檔案的時候,在大多數情況下其實都是非同步的,因此 Schematics 在 v8.0.0
的時候就已經將這支 API 棄用了:
/**
* @deprecated Since v8.0.0 - Use {@link SchematicTestRunner.runSchematicAsync} instead.
* All schematics can potentially be async.
* This synchronous variant will fail if the schematic, any of its rules, or any schematics
* it calls are async.
*/
runSchematic<SchematicSchemaT>(schematicName: string, opts?: SchematicSchemaT, tree?: Tree): UnitTestTree;
如上所述,所以從 v8.0.0
之後,就改為使用 runSchematicAsync
與 runExternalSchematicAsync
這兩支 API ,所以我們可以將其改成:
describe('hello-world', () => {
// 略
it('成功在預設專案路徑底下產出檔案', () => {
const options: HelloWorldSchema = { name: 'feature/Leo Chen' };
const runner = new SchematicTestRunner('schematics', collectionPath);
runner.runExternalSchematicAsync(
'@schematics/angular',
'workspace',
workspaceOptions
).pipe(
mergeMap((tree) => {
return runner.runExternalSchematicAsync(
'@schematics/angular',
'application',
appOptions,
tree
);
}),
mergeMap((tree) => runner.runSchematicAsync('hello-world', options, tree))
).subscribe((tree) => {
expect(tree.files).toContain('/projects/hello/src/app/feature/hello-leo-chen.component.ts');
});
});
});
雖然筆者滿喜歡用 RxJS ,但個人覺得在測試裡用有點殺雞用牛刀,而且乍看之下有種測試程式碼比原本的程式碼還要多的感覺,沒有那麼適當。
所以筆者再分享一招給大家,那就是使用 async
與 await
。
關於
async
與await
的知識,可以參考 ES7 Async Await 聖經與鐵人賽:JavaScript Await 與 Async 這兩篇文章
筆者覺得 async
與 await
非常適合在測試程式裡使用,畢竟一般不管是自己在 Coding 或是看別人的 Code 的時候,還是會比較習慣由上而下、由左至右地的方式。
話不多說,立馬來使用 async
與 await
改寫:
describe('hello-world', () => {
// 略
it('成功在預設專案路徑底下產出檔案', async () => {
const options: HelloWorldSchema = { name: 'feature/Leo Chen' };
const runner = new SchematicTestRunner('schematics', collectionPath);
let appTree = await runner.runExternalSchematicAsync(
'@schematics/angular',
'workspace',
workspaceOptions
).toPromise();
appTree = await runner.runExternalSchematicAsync(
'@schematics/angular',
'application',
appOptions,
appTree
).toPromise();
appTree = await runner.runSchematicAsync('hello-world', options, appTree).toPromise();
expect(appTree.files).toContain('/projects/hello/src/app/feature/hello-leo-chen.component.ts');
});
});
如何?是不是更直觀了些?
除了預設專案的情境之外,我們也可以再測試另一個專案的情境,看看我們是不是真的可以指定專案路徑來產生檔案。
首先先新增另一組 it
,並依樣畫葫蘆地加上測試程式碼:
// 略
describe('hello-world', () => {
// 略
it('成功在預設專案路徑底下產出檔案', async () => {
// 略
});
it('成功在 "world" 專案路徑底下產出檔案', async () => {
const options: HelloWorldSchema = { name: 'feature/Leo Chen', project: 'world' };
const runner = new SchematicTestRunner('schematics', collectionPath);
let appTree = await runner.runExternalSchematicAsync(
'@schematics/angular',
'workspace',
workspaceOptions
).toPromise();
appTree = await runner.runExternalSchematicAsync(
'@schematics/angular',
'application',
appOptions,
appTree
).toPromise();
appTree = await runner.runExternalSchematicAsync(
'@schematics/angular',
'application',
{ ...appOptions, name: 'world' },
appTree
).toPromise();
appTree = await runner.runSchematicAsync('hello-world', options, appTree).toPromise();
expect(appTree.files).toContain('/projects/world/src/app/feature/hello-leo-chen.component.ts');
});
});
但這樣會有太多重複的程式碼,重構一下:
describe('hello-world', () => {
const runner = new SchematicTestRunner('schematics', collectionPath);
const workspaceOptions: WorkspaceOptions = {
name: 'workspace',
newProjectRoot: 'projects',
version: '6.0.0',
};
const appOptions: ApplicationOptions = {
name: 'hello',
inlineStyle: false,
inlineTemplate: false,
routing: false,
style: Style.Css,
skipTests: false,
skipPackageJson: false,
};
const defalutOptions: HelloWorldSchema = {
name: 'feature/Leo Chen'
};
let appTree: UnitTestTree;
beforeEach(async () => {
appTree = await runner.runExternalSchematicAsync(
'@schematics/angular',
'workspace',
workspaceOptions
).toPromise();
appTree = await runner.runExternalSchematicAsync(
'@schematics/angular',
'application',
appOptions,
appTree
).toPromise();
});
it('成功在預設專案路徑底下產出檔案', async () => {
const options = { ...defalutOptions };
const tree = await runner.runSchematicAsync('hello-world', options, appTree).toPromise();
expect(tree.files).toContain('/projects/hello/src/app/feature/hello-leo-chen.component.ts');
});
it('成功在 "world" 專案路徑底下產出檔案', async () => {
appTree = await runner.runExternalSchematicAsync(
'@schematics/angular',
'application',
{ ...appOptions, name: 'world' },
appTree
).toPromise();
const options = { ...defalutOptions, project: 'world' };
const tree = await runner.runSchematicAsync('hello-world', options, appTree).toPromise();
expect(tree.files).toContain('/projects/world/src/app/feature/hello-leo-chen.component.ts');
});
});
雖然還有很多地方可以再重構,不過筆者覺得,在 it
裡還是要能看得出基本的 3A原則 會比較好。
3A 原則 指的是:
Arrange
- 初始化目標物件、相依物件、方法參數、預期結果,或是預期與相依物件的互動方式等等。Act
- 呼叫目標物件的方法。Assert
- 驗證結果是否符合預期。
結果:
今天的程式碼:https://github.com/leochen0818/angular-schematics-30days-challenge/tree/day09
今天是本系列文第三次寫測試了,大家是否有越來越熟悉了呢?!與其放綠乖乖保平安,還不如多寫些測試案例並把它都變綠燈來得更加實在!
目前為了讓大家逐漸熟悉寫測試,所以都會採用先開發、後測試的方式來撰寫文章;後續會慢慢調整成先寫測試再開發的方式,畢竟通常都是先有規格,才會開始開發。特例就算了(笑)。
到目前為止,我們已經學會了如何新增程式碼,之後要開始學習如何編輯程式碼,為此,筆者明天將分享給大家一個非常強大的 Libaray 給大家,敬請期待。